/**
 * 翻译自 https://github.com/smithbower/JS-Swarm 的 Biod 部分
 * 一个 boid 是一个小型独立集群里面的实体。 biods 遵循下面三个原则：
 *
 * 1) 以一个和群方向大致相同的方向行动。
 * 2) 向群的中心移动。
 * 3) 不会太过靠近别的个体。
 *
 * 我们还有一些附加功能。
 * 我们支持 Emitters，它可以持续产生吸引力或排斥力（类似于飞蛾对火焰）。
 * Predators 通过创造一个移动的排斥力 Emitter 来模拟。（在这个库中删除了 Predator 类）
 *
 * 我们还限制了转弯速度（Boid 必须转向新的方向），这使得运动更加自然，并创造出漂亮的弧线.
 *
 * 我们的 Boid 对旁边的 Boid 也有限制 —— Boid 有一个视距半径（他们能看到周围的距离），我们使用点积来确保 Boid 只“看到”Boid 前面的 Boid（尽管我们可以有180度的视野，哈哈）
 */
import { Point, Vector } from "./Vector";

class Boid {
    majorInfluence?: string; //Tracks which force is dominating our heading, for diagnostics.
    majorInfluenceStrength?: number;

    emitterInfluences: Vector[] = []; //A list of various influences affecting us.

    heading = new Vector(1, 0);
    sandboxDimensions: Vector;
    position: Vector;

    updatePosition: (...args: any[]) => Point;
    /**
     * @param updatePosition 定义如何更新对象的位置
     * @param speed 行动速度
     * @param turnSpeed 转向速度
     * @param radius 身体半径
     * @param proximityRadius 排斥力半径
     * @param sightRadius 视野半径
     * @param claustrophobicSeverity 个体排斥力
     * @param sandboxDimensions 沙盒区域
     * */
    constructor(
        updatePosition: (...args: any[]) => Point,
        public speed: number,
        public turnSpeed: number,
        public radius: number,
        public proximityRadius: number,
        public sightRadius: number,
        public claustrophobicSeverity: number,
        sandboxDimensions: Point
    ) {
        this.updatePosition = updatePosition;
        const { x, y } = updatePosition();
        this.position = new Vector(x, y);
        this.sandboxDimensions = new Vector(
            sandboxDimensions.x,
            sandboxDimensions.y
        );
    }
    /**
     * Given a new heading, figures out how much rotation is required/allowed
     * between new heading and current heading, and then moves us forward.
     *
     * Also wraps us around the edges of the sandbox.
     */
    move(newHeading: Vector) {
        //Rotate current heading to new heading by rotation speed.
        //Limited rotation speed gives the nice swooping effect.
        const newAngle = newHeading.toDegrees();
        const currentAngle = this.heading.toDegrees();

        //Figure out if it's quicker to turn left or right.
        const left = (newAngle - currentAngle + 360) % 360;
        const right = (currentAngle - newAngle + 360) % 360;

        //Compute how much we need to turn, capped by max turn speed.
        const rotationAmount =
            left < right
                ? Math.min(this.turnSpeed, left)
                : -Math.min(this.turnSpeed, right);

        //Turn.
        this.heading.fromDegrees((currentAngle + rotationAmount + 360) % 360);

        //Compute new position.
        const positionOffset = this.heading.scale(this.speed);
        this.position.x =
            (this.position.x + positionOffset.x) % this.sandboxDimensions.x;
        this.position.y =
            (this.position.y + positionOffset.y) % this.sandboxDimensions.y;

        //Wrap around the sandbox.
        if (this.position.x < 0) this.position.x = this.sandboxDimensions.x;
        if (this.position.y < 0) this.position.y = this.sandboxDimensions.y;
    }
    isInVision(boid: Boid) {
        return boid.position.getDistance(this.position) <= this.proximityRadius;
    }
    /**
     * Computes a new heading for the boid based on the rest of the flock. Computes visible neighbors,
     * calculates their forces on the new heading, takes in to account forces from emitters.
     */
    calculateNewHeading(flock: Boid[]) {
        // var neighbors = [];
        var claustrophobia = new Vector(0, 0),
            avgHeading = new Vector(0, 0),
            newHeading = new Vector(0, 0),
            avgPosition = new Vector(0, 0);

        var claustroCount = 0;

        //Find *visible* neighbors within the flock.
        // 在群中寻找自己看得见的邻居
        const neighbors = flock.flatMap((single) => {
            var canBeSeen = Math.cos(
                this.heading.dot(this.position.getDirectionTo(single.position))
            ); //Make sure neighbor is in front of us.

            if (
                single.position.getDistance(this.position) <=
                    this.sightRadius &&
                single != this &&
                canBeSeen > 0
            ) {
                return [single];
            } else {
                return [];
            }
        });

        // Calculate repulsion factor from neighbors.
        // 从同伴中计算排斥系数
        neighbors.forEach((single: Boid) => {
            if (this.isInVision(single)) {
                //Calculate radial point for proximity radius.
                var proximityPoint = this.position.getDirectionTo(
                    single.position
                );
                proximityPoint.scale(this.proximityRadius);

                //f = -kd, except we don't need the neg because we're just going to use the flipped dir.
                var dist = single.position.getDistance(proximityPoint);
                var force = dist * this.claustrophobicSeverity;

                //Cheat to keep us from intersecting too much.
                if (dist < this.radius * 1.1) force += 1000000;

                claustrophobia = claustrophobia.add(
                    single.position.getDirectionTo(this.position).scale(force)
                );
                claustroCount++;
            }

            //Also average heading and position of neighbors.
            avgHeading.inplaceAdd(single.heading);
            avgPosition.inplaceAdd(single.position);
        });

        //Average the repulsion strength.
        if (claustrophobia.x != 0) claustrophobia.x /= claustroCount;
        if (claustrophobia.y != 0) claustrophobia.y /= claustroCount;

        //Normalized heading of neighbors (same as avg).
        avgHeading = avgHeading.normalize();

        //Average position of neighbors.
        if (avgPosition.x != 0) avgPosition.x /= neighbors.length;
        if (avgPosition.y != 0) avgPosition.y /= neighbors.length;
        avgPosition = this.position.getDirectionTo(avgPosition);

        //Calculate emitter influences.
        for (var i = 0; i < this.emitterInfluences.length; i++)
            newHeading.inplaceAdd(this.emitterInfluences[i]);

        //Figure out which force is dominant.
        var claustroFactor = claustrophobia.length();
        var cohesionFactor = avgHeading.length() + avgPosition.length();
        var emitterFactor = newHeading.length();

        if (
            claustroFactor >= cohesionFactor &&
            claustroFactor >= emitterFactor
        ) {
            this.majorInfluence = "claustrophobia";
            this.majorInfluenceStrength = claustroFactor;
        } else {
            if (cohesionFactor >= emitterFactor) {
                this.majorInfluence = "cohesion";
                this.majorInfluenceStrength = cohesionFactor;
            } else {
                this.majorInfluence = "emitter";
                this.majorInfluenceStrength = emitterFactor;
            }
        }

        //Average all the forces to calculate a new heading.
        newHeading
            .inplaceAdd(avgHeading)
            .inplaceAdd(claustrophobia)
            .inplaceAdd(avgPosition);

        newHeading.x /= 3 + this.emitterInfluences.length; //3 because we have 3 major forces acting on us.
        newHeading.y /= 3 + this.emitterInfluences.length;

        newHeading = newHeading.normalize(); //Convert to a direction.

        //Make sure to clear out emitter influences list.
        this.emitterInfluences.length = 0;

        return newHeading;
    }
}

export { Boid };
